/*******************************************************************************
 * Copyright 2011-2012 Dik Grapendaal
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package sh.grapendaal.tsuushin.core;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * This class will assist in selecting the right bot(s) for the right job at the
 * right time in the right location. An instance with the current bot list can
 * be obtained from the Bot Service. The filter methods return the instance of
 * the current filter, to enable a fluent interface.
 */
public class BotFilter {
	private List<IrcBot> bots;
	private List<IrcBot> filteredBots;
	private Iterator<IrcBot> filteredBotIterator;
	private Criteria criteria = new Criteria();

	/**
	 * Constructs a new BotFilter instance which will filter on the given list
	 * of bots.
	 * 
	 * @param bots
	 *            The bot list we will be filtering on.
	 */
	public BotFilter(List<IrcBot> bots) {
		this.bots = bots;
	}

	/**
	 * For performance reasons, we filter when we actually want results. This
	 * method does just that.
	 */
	private void doFilter() {
		if (filteredBots == null) {
			filteredBots = new ArrayList<IrcBot>();
			for (IrcBot bot : bots) {
				if (criteria.checkCriteria(bot)) {
					filteredBots.add(bot);
				}
			}
		}
	}

	/**
	 * Filters the current list of bots to bots which are connected to the
	 * specified network. Note that, depending on the configuration, not all
	 * bots on the same are in the same channels. To filter on bots which are in
	 * a specific channel, use this filter in combination with the channel
	 * filter.
	 * 
	 * @param network
	 *            The network name to filter on.
	 * @return Reference to the current BotFilter instance to enable method
	 *         chaining.
	 */
	public BotFilter network(String network) {
		criteria.setNetworkCriterion(network);
		filteredBots = null;
		return this;
	}

	/**
	 * Filters the current list of bots to bots which are in the given channel.
	 * This works across multiple networks. To prevent this, use this filter in
	 * combination with the network filter.
	 * 
	 * @param channel
	 *            The channel to filter on.
	 * @return Reference to the current BotFilter instance to enable method
	 *         chaining.
	 */
	public BotFilter channel(String channel) {
		criteria.setChannelCriterion(channel);
		filteredBots = null;
		return this;
	}

	/**
	 * Returns the number of found bots after applying the specified filters.
	 * 
	 * @return The number of bots.
	 */
	public int count() {
		doFilter();
		return filteredBots.size();
	}

	/**
	 * Gets the first bot from the list of bots that match the specified
	 * criteria.
	 * 
	 * @return The first found bot.
	 */
	public IrcBot getFirstBot() {
		doFilter();
		return filteredBots.get(0);
	}

	/**
	 * Returns the next bot in the matched bot list. This can be used to
	 * distribute a large number of messages over two or more connected bots
	 * easily.
	 * 
	 * @return The next IrcBot to send a message with.
	 */
	public IrcBot getNextBot() {
		// TODO How will this behave over extended periods of time and
		// disconnected/reconnected bots?
		doFilter();
		if (filteredBotIterator == null || !filteredBotIterator.hasNext()) {
			filteredBotIterator = filteredBots.iterator();
		}
		return filteredBotIterator.next();
	}

	/**
	 * Returns a random bot in the matched list of bots.
	 * 
	 * @return An IrcBot to send a message with.
	 */
	public IrcBot getRandomBot() {
		doFilter();
		return null;
	}

	/**
	 * Returns the list with matched bots.
	 * 
	 * @return The list with matched bots.
	 */
	public List<IrcBot> getAllBots() {
		doFilter();
		return filteredBots;
	}

	/**
	 * This class will keep track of all the specified criteria so we can
	 * perform a filter whenever we want.
	 */
	private class Criteria {
		private NetworkCriterion network;
		private ChannelCriterion channel;

		/**
		 * Checks whether the set up criteria match for the given bot. If so,
		 * returns true.
		 * 
		 * @param bot
		 *            The bot to check.
		 * @return Result of the criteria check.
		 */
		public boolean checkCriteria(IrcBot bot) {
			boolean result = true;

			if (network != null) {
				result &= network.checkCriteria(bot);
			}
			if (channel != null) {
				result &= channel.checkCriteria(bot);
			}

			return result;
		}

		/**
		 * Sets the network name to check for.
		 * 
		 * @param value
		 *            The network name.
		 */
		public void setNetworkCriterion(String value) {
			if (network == null) {
				network = new NetworkCriterion();
			}
			network.setValue(value);
		}

		/**
		 * Sets the channel name to check for.
		 * 
		 * @param value
		 *            The channel name.
		 */
		public void setChannelCriterion(String value) {
			if (channel == null) {
				channel = new ChannelCriterion();
			}
			channel.setValue(value);
		}
	}

	/**
	 * Checks whether the network criterion matches.
	 */
	private class NetworkCriterion extends AbstractCriterion {
		/**
		 * Checks whether the setup network name is the same as the network name
		 * the given bot is connected to.
		 * 
		 * {@inheritDoc}
		 */
		@Override
		public boolean checkCriteria(IrcBot bot) {
			return bot.getNetwork().getName().equals(this.getValue());
		}
	}

	/**
	 * Checks whether the channel criterion matches.
	 */
	private class ChannelCriterion extends AbstractCriterion {
		/**
		 * Checks whether the given IrcBot has joined the setup channel.
		 * Comparing is done in a case sensitive way. This will change to case
		 * insensitive in the future because the IRC protocol is case
		 * insensitive as well.
		 * 
		 * {@inheritDoc}
		 */
		@Override
		public boolean checkCriteria(IrcBot bot) {
			return bot.getChannels().contains(new Channel(this.getValue()));
		}
	}

	/**
	 * Checks whether the specified criterion matches.
	 */
	private abstract class AbstractCriterion implements Criterion {
		private String value;

		/**
		 * {@inheritDoc}
		 */
		@Override
		public String getValue() {
			return value;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public void setValue(String value) {
			this.value = value;
		}
	}

	/**
	 * Interface for a class that checks whether an IrcBot object matches the
	 * specified criterion.
	 */
	private interface Criterion {
		/**
		 * Returns the set value which need to match in the given IrcBot.
		 * 
		 * @return The set value.
		 */
		String getValue();

		/**
		 * Sets the value we want to see in the matched result.
		 * 
		 * @param value
		 *            The value to check for.
		 */
		void setValue(String value);

		/**
		 * The place where the actual comparing is done with the expected value
		 * and the actual value.
		 * 
		 * @param bot
		 *            The bot to check.
		 * @return The result of the check.
		 */
		boolean checkCriteria(IrcBot bot);
	}
}
